# userinput.py
"""
Utilities for collecting interactive information from users.
"""

import os, string
import iso639, iso3166
iso3166_countrycode_extended = iso3166._iso_countrycode
iso3166_countrycode_extended['ZZ'] = ("Universal", "ZZZ", -1)

class UserInputError(ValueError):
    pass


def yn_inp(response):
    """
    Classify user response as 'yes'/True or 'no'/False.
    
    >>> yn_inp('Y')
    True
    >>> yn_inp("no")
    False
    >>> yn_inp("")
    >>> yn_inp('false')
    False
    >>> 
    """
    if response in ("Y", "y", "yes", "Yes", "1", "T", "True", "true"):
        return True
    elif response in ("N", "n", "no", "No", "0", "F", "False", "false"):
        return False
    else:
        return None

def yn_out(state):
    """
    >>> yn_out(False)
    'y/N'
    >>> yn_out(True)
    'Y/n'
    >>> yn_out(None)
    'y/n'
    >>> 
    """
    return {True:'Y/n', False:'y/N', None:'y/n'}[state]


   
def timecode(response):
    """
    Creates a valid timecode from sloppy input.
    Format: HH:MM:SS.mmm
    (H-hours, M-minutes, S-seconds, m-millis).
    
    >>> timecode("4:33.45")
    '00:04:33.450'
    >>> timecode("foo:bar:baz:boo")
    Traceback (most recent call last):
      ...
    UserInputError: Timecodes must be in 'HH:MM:SS.mmm' format (Hours, Minutes, Seconds, Milliseconds).
    >>> timecode("spam:ham:bacon")
    Traceback (most recent call last):
      ...
    ValueError: could not convert string to float: bacon
    >>> timecode("00:04:33.450")
    '00:04:33.450'
    >>>
    """
    fields = response.strip().split(':')
    if len(fields)>3:
        raise UserInputError(
          "Timecodes must be in 'HH:MM:SS.mmm' format (Hours, Minutes, Seconds, Milliseconds).")
    seconds = float(fields[-1])
    if len(fields)>1:
        minutes = int(fields[-2])
    else:
        minutes = 0
    if len(fields)>2:
        hours = int(fields[-3])
    else:
        hours = 0
    timecode = "%2.2d:%2.2d:%2.3f" % (hours, minutes, seconds)
    return timecode

    

def file_inp(response):
    """
    Checks validity for input files.
    
    >>> file_inp("dummypngs")
    'dummypngs'
    >>> file_inp("spam")
    Traceback (most recent call last):
      ...
    UserInputError: No such file exists - spam
    Wrong path?
    >>>    
    """
    if os.path.exists(response):
        return os.path.normpath(response)
    else:
        raise UserInputError("No such file exists - %s\nWrong path?" % response)


        
def new_file_inp(response):
    """
    Checks validity for output files.
    
    >>> new_file_inp('feature.lrv')
    'feature.lrv'
    >>> new_file_inp('./feature.lrv')
    'feature.lrv'
    >>>
    """
    if os.path.exists(response):
        print "WARNING - file '%s' exists already, it will be overwritten." % response
    folder = os.path.dirname(response)
    if not folder:
        folder = os.getcwd()
    if os.path.exists(folder) and os.path.isdir(folder):
        return os.path.normpath(response)
    else:
        raise UserInputError("No such directory exists - %s\nWrong path?" % folder)

        
        
language_code_help = """\
    Code must be an ISO-639-x code (like 'en' or 'eng' for 'English').
    and will be converted to ISO-639-3.
    Some useful special cases:
        'mul' --> Multiple / Mixed Language.
        'zxx' --> No language (e.g. music or sound effects only).
        'und' --> Undetermined language (i.e. you don't know what language).
        'mis' --> A definite language, but there is no ISO-639 code for it.
    """

    
    
def language_inp(response):
    """
    Validate and convert ISO-639 codes.
    Return valid ISO-639-3 code or raises exception.
    
    >>> language_inp('en')
    'en'
    >>> language_inp('qq')    # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
      ...
    UserInputError: Language code 'qq' not recognized.
      ...
    >>>
    """
    response = response.lower()
    if response in iso639.iso639_to_name:
        return response
    else:
        raise UserInputError(
          ("Language code '%s' not recognized." % response) + language_code_help)

          
          
def language_out(code):
    """
    Does ISO-639 look-up to show correct language mapping.
    
    >>> language_out('en')
    'en (English)'
    >>> language_out('zxx')
    'zxx (No linguistic content)'
    >>> 
    """
    return "%s (%s)" % (code, iso639.iso639_to_name[code])

    
    
country_code_help = """\
    ISO-3166-1 Two-Letter Country Codes are expected,
    Such as "US" for "United States".
    You may use the reserved code "ZZ" to indicate "no region" or "universal".
    """

    
    
def country_inp(response):
    """
    Validate and convert correct ISO-3166 Country Codes.
    
    >>> country_inp("GB")
    'GB'
    >>> country_inp("US")
    'US'
    >>> country_inp("ZQ")       # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
      ...
    UserInputError: Country code 'ZQ' not recognized. 
      ...
    >>> 
    """
    response = response.upper()
    if response in iso3166_countrycode_extended:
        return response
    else:
        raise UserInputError(
          ("Country code '%s' not recognized." % response) + country_code_help)

          
          

def country_out(code):
    """
    Does ISO-3166 look-up for user feedback.
    
    >>> country_out('US')
    'US (United States)'
    >>> country_out('ZZ')
    'ZZ (Universal)'
    >>> country_out('ZQ')   # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
      ...
    KeyError: 'ZQ'
    >>> 
    """
    return "%s (%s)" % (code, iso3166._iso_countrycode[code][0])
    
    
    
def ident_inp(response):
    """
    Make a general string response safe for use as an identifier.
    
    >>> ident_inp(" & then there was 1")
    'then_there_was_1'
    >>> ident_inp("A'")
    'A'
    >>> ident_inp("")
    '_'
    >>> ident_inp("4 score & 7")
    'score_7'
    >>> 
    >>> ident_inp("3 Rings for the Elven Kings under the stars, 7 for the Dwarf Lords in their halls of stone")
    'Rings_for_the_Elven_Kings_under_'
    >>>
    """
    if not response:
        return '_'
    safe_char1 = string.letters + '_'
    safe_chars = safe_char1 + string.digits + string.whitespace
    firstchar = [a for a in response if a in safe_char1][0]
    response = response[response.find(firstchar)+1:]
    response = firstchar + ''.join([a for a in response if a in safe_chars])
    response = '_'.join(response.split())
    return response[:32]
    
    
    
def userinput(prompt, default=None, inp=unicode, out=unicode, test=None):
    """
    Ask the user a question and return the response in a desired format.
    Wraps the 'raw_input' built-in with in/out filters and defaulting behavior.
    
    FIXME: Tests w/ user input have to be run manually:
    >>> userinput("Do you wish to be clear?", default=False, inp=yn_inp, out=yn_out, test="maybe")
    Do you wish to be clear? [y/N] <-- maybe
    False
    >>> 
    """
    while True:
        try:
            if test is not None:
                print "%s [%s] <-- %s" % (prompt, out(default), test)
                response = test
            else:
                response = raw_input(prompt + (" [%s] " % out(default)))
            if not response:
                return default
            response = inp(response)
            if response is not None:
                return response
            elif default is not None:
                return default
            else:
                raise UserInputError("No safe default -- answer required for this question.")
        except UserInputError, e:
            print "The wizard is not that wise:\n\t%s" % str(e)
            print "Please try again."

            

def userselect(prompt, options, default=None, test=False):
    """
    Ask the user to select from a set of options by entering a number.
    
    FIXME: Tests w/ user input have to be run manually:
    
    >>> userselect("Pick one:", ("A", "B", "C"), default=0, test="2")
    Pick one:
      Options:
    1 A
    2 B
    3 C
    ? [A] <-- 2
    1
    >>> 
    """
    while True:
        print prompt
        print "  Options:"
        for i,option in enumerate(options):
            print i+1, option
        try:
            if test is not None:
                print "%s [%s] <-- %s" % ("?", options[default], test)
                response = test
            else:
                response = raw_input("? [%s]" % options[default])
            if not response and default is not None:
                return default
            try:
                response = int(response)
            except ValueError:
                if default is not None:
                    return default
                else:
                    continue
            if response in range(1,len(options)+1):
                return response - 1
            else:
                raise UserInputError("No option '%d'. Must be (1-%d)." % (response, len(options)))
        except UserInputError, e:
            print "\t" + str(e)
            print "Please try again." 
            
            
if __name__ == "__main__":
    import doctest
    doctest.testmod()

